# -*- coding: utf-8 -*-

# Copyright (c) 2022 - 2025 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing a dialog to enter the parameters for an isort formatting run.
"""

import copy
import pathlib

import tomlkit

from isort import Config
from isort.profiles import profiles
from isort.settings import VALID_PY_TARGETS
from isort.wrap_modes import WrapModes
from PyQt6.QtCore import pyqtSlot
from PyQt6.QtGui import QGuiApplication
from PyQt6.QtWidgets import QDialog, QDialogButtonBox

from eric7.EricWidgets import EricMessageBox
from eric7.EricWidgets.EricApplication import ericApp

from .Ui_IsortConfigurationDialog import Ui_IsortConfigurationDialog


# TODO: add support for 'known_local_folder'
#       example: known_local_folder = ["my_module1", "my_module2"]
class IsortConfigurationDialog(QDialog, Ui_IsortConfigurationDialog):
    """
    Class implementing a dialog to enter the parameters for an isort formatting run.
    """

    def __init__(self, withProject=True, onlyProject=False, parent=None):
        """
        Constructor

        @param withProject flag indicating to look for project configurations
            (defaults to True)
        @type bool (optional)
        @param onlyProject flag indicating to only look for project configurations
            (defaults to False)
        @type bool (optional)
        @param parent reference to the parent widget (defaults to None)
        @type QWidget (optional)
        """
        super().__init__(parent)
        self.setupUi(self)

        self.profileComboBox.lineEdit().setClearButtonEnabled(True)

        self.__parameterWidgetMapping = {
            "profile": self.profileComboBox,
            "py_version": self.pythonComboBox,
            "multi_line_output": self.multiLineComboBox,
            "sort_order": self.sortOrderComboBox,
            "supported_extensions": self.extensionsEdit,
            "line_length": self.lineLengthSpinBox,
            "lines_before_imports": self.linesBeforeImportsSpinBox,
            "lines_after_imports": self.linesAfterImportsSpinBox,
            "lines_between_sections": self.linesBetweenSectionsSpinBox,
            "lines_between_types": self.linesBetweenTypesSpinBox,
            "include_trailing_comma": self.trailingCommaCheckBox,
            "use_parentheses": self.parenthesesCheckBox,
            "combine_as_imports": self.combineAsCheckBox,
            "sections": self.sectionsEdit,
            "extend_skip_glob": self.excludeEdit,
            "case_sensitive": self.sortCaseSensitiveCheckBox,
            "force_sort_within_sections": self.sortIgnoreStyleCheckBox,
            "from_first": self.sortFromFirstCheckBox,
            "known_first_party": self.firstPartyEdit,
        }

        self.__project = (
            ericApp().getObject("Project") if (withProject or onlyProject) else None
        )
        self.__onlyProject = onlyProject

        self.__pyprojectData = {}
        self.__projectData = {}

        self.__defaultConfig = Config()

        self.__tomlButton = self.buttonBox.addButton(
            self.tr("Generate TOML"), QDialogButtonBox.ButtonRole.ActionRole
        )
        self.__tomlButton.setToolTip(
            self.tr("Place a code snippet for 'pyproject.toml' into the clipboard.")
        )
        self.__tomlButton.clicked.connect(self.__createTomlSnippet)

        self.profileComboBox.addItem("")
        self.profileComboBox.addItems(sorted(profiles))

        self.pythonComboBox.addItem("", "")
        self.pythonComboBox.addItem(self.tr("All Versions"), "all")
        for pyTarget in VALID_PY_TARGETS:
            if pyTarget.startswith("3"):
                self.pythonComboBox.addItem(
                    (
                        self.tr("Python {0}").format(pyTarget)
                        if len(pyTarget) == 1
                        else self.tr("Python {0}.{1}").format(pyTarget[0], pyTarget[1:])
                    ),
                    pyTarget,
                )

        self.sortOrderComboBox.addItem("", "")
        self.sortOrderComboBox.addItem("Natural", "natural")
        self.sortOrderComboBox.addItem("Native Python", "native")

        self.__populateMultiLineComboBox()

        # setup the source combobox
        self.sourceComboBox.addItem("", "")
        if self.__project:
            pyprojectPath = (
                pathlib.Path(self.__project.getProjectPath()) / "pyproject.toml"
            )
            if pyprojectPath.exists():
                try:
                    with pyprojectPath.open("r", encoding="utf-8") as f:
                        data = tomlkit.load(f)
                    config = data.get("tool", {}).get("isort", {})
                except (OSError, tomlkit.exceptions.ParseError):
                    config = {}
                if config:
                    self.__pyprojectData = {
                        k.replace("--", ""): v for k, v in config.items()
                    }
                    self.sourceComboBox.addItem("pyproject.toml", "pyproject")
            if self.__project.getData("OTHERTOOLSPARMS", "isort") is not None:
                self.__projectData = copy.deepcopy(
                    self.__project.getData("OTHERTOOLSPARMS", "isort")
                )
                self.sourceComboBox.addItem(self.tr("Project File"), "project")
            elif onlyProject:
                self.sourceComboBox.addItem(self.tr("Project File"), "project")
        if not onlyProject:
            self.sourceComboBox.addItem(self.tr("Defaults"), "default")
            self.sourceComboBox.addItem(self.tr("Configuration Below"), "dialog")

        if self.__projectData:
            source = self.__projectData.get("config_source", "")
            self.sourceComboBox.setCurrentIndex(self.sourceComboBox.findData(source))
        elif onlyProject:
            self.sourceComboBox.setCurrentIndex(self.sourceComboBox.findData("project"))

    def __populateMultiLineComboBox(self):
        """
        Private method to populate the multi line output selector.
        """
        self.multiLineComboBox.addItem("", -1)
        for entry, wrapMode in (
            (self.tr("Grid"), WrapModes.GRID),
            (self.tr("Vertical"), WrapModes.VERTICAL),
            (self.tr("Hanging Indent"), WrapModes.HANGING_INDENT),
            (
                self.tr("Vertical Hanging Indent"),
                WrapModes.VERTICAL_HANGING_INDENT,
            ),
            (self.tr("Hanging Grid"), WrapModes.VERTICAL_GRID),
            (self.tr("Hanging Grid Grouped"), WrapModes.VERTICAL_GRID_GROUPED),
            (self.tr("NOQA"), WrapModes.NOQA),
            (
                self.tr("Vertical Hanging Indent Bracket"),
                WrapModes.VERTICAL_HANGING_INDENT_BRACKET,
            ),
            (
                self.tr("Vertical Prefix From Module Import"),
                WrapModes.VERTICAL_PREFIX_FROM_MODULE_IMPORT,
            ),
            (
                self.tr("Hanging Indent With Parentheses"),
                WrapModes.HANGING_INDENT_WITH_PARENTHESES,
            ),
            (self.tr("Backslash Grid"), WrapModes.BACKSLASH_GRID),
        ):
            self.multiLineComboBox.addItem(entry, wrapMode.value)

    def __loadConfiguration(self, confDict):
        """
        Private method to load the configuration section with data of the given
        dictionary.

        Note: Default values will be loaded for missing parameters.

        @param confDict reference to the data to be loaded
        @type dict
        """
        self.pythonComboBox.setCurrentIndex(
            self.pythonComboBox.findData(
                str(confDict["py_version"])
                if "py_version" in confDict
                else self.__defaultConfig.py_version.replace("py", "")
            )
        )
        self.multiLineComboBox.setCurrentIndex(
            self.multiLineComboBox.findData(
                int(confDict["multi_line_output"])
                if "multi_line_output" in confDict
                else self.__defaultConfig.multi_line_output.value
            )
        )
        self.sortOrderComboBox.setCurrentIndex(
            self.sortOrderComboBox.findData(
                str(confDict["sort_order"])
                if "sort_order" in confDict
                else self.__defaultConfig.sort_order
            )
        )
        self.extensionsEdit.setText(
            " ".join(
                confDict["supported_extensions"]
                if "supported_extensions" in confDict
                else self.__defaultConfig.supported_extensions
            )
        )
        for parameter in (
            "line_length",
            "lines_before_imports",
            "lines_after_imports",
            "lines_between_sections",
            "lines_between_types",
        ):
            # set spin box values
            self.__parameterWidgetMapping[parameter].setValue(
                confDict[parameter]
                if parameter in confDict
                else getattr(self.__defaultConfig, parameter)
            )
        for parameter in (
            "include_trailing_comma",
            "use_parentheses",
            "combine_as_imports",
            "case_sensitive",
            "force_sort_within_sections",
            "from_first",
        ):
            # set check box values
            self.__parameterWidgetMapping[parameter].setChecked(
                confDict[parameter]
                if parameter in confDict
                else getattr(self.__defaultConfig, parameter)
            )
        for parameter in (
            "sections",
            "extend_skip_glob",
            "known_first_party",
        ):
            # set the plain text edits
            self.__parameterWidgetMapping[parameter].setPlainText(
                "\n".join(
                    confDict[parameter]
                    if parameter in confDict
                    else getattr(self.__defaultConfig, parameter)
                )
            )
        # set the profile combo box last because it may change other entries
        self.profileComboBox.setEditText(
            confDict["profile"]
            if "profile" in confDict
            else self.__defaultConfig.profile
        )

    @pyqtSlot(str)
    def on_sourceComboBox_currentTextChanged(self, selection):
        """
        Private slot to handle the selection of a configuration source.

        @param selection text of the currently selected item
        @type str
        """
        self.buttonBox.button(QDialogButtonBox.StandardButton.Ok).setEnabled(
            bool(selection) or self.__onlyProject
        )

        source = self.sourceComboBox.currentData()
        if source != "dialog":
            # reset the profile combo box first
            self.profileComboBox.setCurrentIndex(0)

        if source == "pyproject":
            self.__loadConfiguration(self.__pyprojectData)
        elif source == "project":
            self.__loadConfiguration(self.__projectData)
        elif source == "default":
            self.__loadConfiguration({})  # loads the default values
        elif source == "dialog":
            # just leave the current entries
            pass

    @pyqtSlot(str)
    def on_profileComboBox_editTextChanged(self, profileName):
        """
        Private slot to react upon changes of the selected/entered profile.

        @param profileName name of the current profile
        @type str
        """
        if profileName and profileName in profiles:
            confDict = self.__getConfigurationDict()
            confDict["profile"] = profileName
            confDict.update(profiles[profileName])
            self.__loadConfiguration(confDict)

            for parameter in self.__parameterWidgetMapping:
                self.__parameterWidgetMapping[parameter].setEnabled(
                    parameter not in profiles[profileName]
                )
        else:
            for widget in self.__parameterWidgetMapping.values():
                widget.setEnabled(True)

    @pyqtSlot()
    def __createTomlSnippet(self):
        """
        Private slot to generate a TOML snippet of the current configuration.

        Note: Only non-default values are included in this snippet.

        The code snippet is copied to the clipboard and may be placed inside the
        'pyproject.toml' file.
        """
        configDict = self.__getConfigurationDict()

        isort = tomlkit.table()
        for key, value in configDict.items():
            isort[key] = value

        doc = tomlkit.document()
        doc["tool"] = tomlkit.table(is_super_table=True)
        doc["tool"]["isort"] = isort

        QGuiApplication.clipboard().setText(tomlkit.dumps(doc))

        EricMessageBox.information(
            self,
            self.tr("Create TOML snippet"),
            self.tr(
                """The 'pyproject.toml' snippet was copied to the clipboard"""
                """ successfully."""
            ),
        )

    def __getConfigurationDict(self):
        """
        Private method to assemble and return a dictionary containing the entered
        non-default configuration parameters.

        @return dictionary containing the non-default configuration parameters
        @rtype dict
        """
        configDict = {}

        if self.profileComboBox.currentText():
            configDict["profile"] = self.profileComboBox.currentText()
        if (
            self.pythonComboBox.currentText()
            and self.pythonComboBox.currentData()
            != self.__defaultConfig.py_version.replace("py", "")
        ):
            configDict["py_version"] = self.pythonComboBox.currentData()
        if self.multiLineComboBox.isEnabled() and self.multiLineComboBox.currentText():
            configDict["multi_line_output"] = self.multiLineComboBox.currentData()
        if self.sortOrderComboBox.isEnabled() and self.sortOrderComboBox.currentText():
            configDict["sort_order"] = self.sortOrderComboBox.currentData()
        if self.extensionsEdit.isEnabled() and self.extensionsEdit.text():
            configDict["supported_extensions"] = [
                e.lstrip(".")
                for e in self.extensionsEdit.text().strip().split()
                if e.lstrip(".")
            ]

        for parameter in (
            "line_length",
            "lines_before_imports",
            "lines_after_imports",
            "lines_between_sections",
            "lines_between_types",
        ):
            if self.__parameterWidgetMapping[
                parameter
            ].isEnabled() and self.__parameterWidgetMapping[
                parameter
            ].value() != getattr(
                self.__defaultConfig, parameter
            ):
                configDict[parameter] = self.__parameterWidgetMapping[parameter].value()

        for parameter in (
            "include_trailing_comma",
            "use_parentheses",
            "combine_as_imports",
            "case_sensitive",
            "force_sort_within_sections",
            "from_first",
        ):
            if self.__parameterWidgetMapping[
                parameter
            ].isEnabled() and self.__parameterWidgetMapping[
                parameter
            ].isChecked() != getattr(
                self.__defaultConfig, parameter
            ):
                configDict[parameter] = self.__parameterWidgetMapping[
                    parameter
                ].isChecked()

        for parameter in (
            "sections",
            "extend_skip_glob",
            "known_first_party",
        ):
            if self.__parameterWidgetMapping[parameter].isEnabled():
                value = (
                    self.__parameterWidgetMapping[parameter].toPlainText().splitlines()
                )
                if value != list(getattr(self.__defaultConfig, parameter)):
                    configDict[parameter] = value

        return configDict

    def getConfiguration(self, saveToProject=False):
        """
        Public method to get the current configuration parameters.

        @param saveToProject flag indicating to save the configuration data in the
            project file (defaults to False)
        @type bool (optional)
        @return dictionary containing the configuration parameters
        @rtype dict
        """
        configuration = self.__getConfigurationDict()

        if saveToProject and self.__project:
            configuration["config_source"] = self.sourceComboBox.currentData()
            self.__project.setData("OTHERTOOLSPARMS", "isort", configuration)

        return configuration
